package jmine.tec.utils.date;

import java.io.Serializable;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import jmine.tec.utils.IterationStep;

/**
 * Classe com métodos utilitários para Date
 * 
 * @author julien
 */
public final class DateUtil {

    /**
     * Iterator interno para converter um iterator de calendar para date.
     * 
     * @author takeshi
     * @version 07/08/2007
     */
    private static final class CalendarToDateIterationStep implements IterationStep<Calendar>, Serializable {
        /**
         * 
         */
        private static final long serialVersionUID = 4667310438921971544L;

        /**
         * 
         */
        private final IterationStep<Date> step;

        /**
         * C'tor
         * 
         * @param step o {@link IterationStep} para ser chamado
         */
        public CalendarToDateIterationStep(final IterationStep<Date> step) {
            this.step = step;
        }

        /**
         * Converte o {@link Calendar} para {@link Date} e chama sobre o {@link IterationStep} de {@link Date} associado.
         * 
         * @param it o {@link Calendar}
         * @return boolean
         * @see br.com.maps.util.IterationStep#nextStep(java.lang.Object)
         */
        public boolean nextStep(final Calendar it) {
            return this.step.nextStep(it.getTime());
        }
    }

    public static final String DATE_PATTERN_DDMMYYYY = "dd/MM/yyyy";

    /**
     * C'tor
     */
    private DateUtil() {
    }

    /**
     * Retorna o Date correspondente criado via Calendar.
     * 
     * @param day int
     * @param month int
     * @param year int
     * @return Date
     */
    public static Date getDate(final int day, final int month, final int year) {
        Calendar cal = Calendar.getInstance();
        clearTimeFields(cal);
        cal.set(year, month, day);
        return cal.getTime();
    }

    /**
     * Retorna o dia de um Date usando Calendar
     * 
     * @param date Date
     * @return int
     */
    public static int getDay(final Date date) {
        Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.setTime(date);
        return cal.get(Calendar.DAY_OF_MONTH);
    }

    /**
     * Adiciona a quantidade passada ao campo da data
     * 
     * @param n a quantidade a se adicionar
     * @param date a data para adicionar
     * @param field o campo em Calendarr da data
     * @return uma nova data com o campo passado alterado
     */
    public static Date add(final int n, final Date date, final int field) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(field, n);
        return calendar.getTime();
    }

    /**
     * Compara duas datas considerando ano e mês apenas.
     * 
     * @param d1 a primeira data a ser comparada
     * @param d2 a segunda data a ser comparada
     * @return -1 se d1 menor que d2, 0 se as datas forem iguais e 1 se d1 maior que d2.
     */
    public static int compareDatesByMonthAndYear(final Date d1, final Date d2) {
        final int cmpYear = compareDatesByField(d1, d2, Calendar.YEAR);
        if (cmpYear != 0) {
            return cmpYear;
        }
        final int cmpMonth = compareDatesByField(d1, d2, Calendar.MONTH);
        return cmpMonth;
    }

    /**
     * Compara duas datas pelos campos de dia, mês e ano.
     * 
     * @param d1 a primeira data a ser comparada
     * @param d2 a segunda data a ser comparada
     * @return -1 se d1 é menor que d2, 1 se d1 é maior que d2 e 0 se ambas são iguais
     */
    public static int compareDatesByDayMonthAndYear(final Date d1, final Date d2) {
        int comp = compareDatesByMonthAndYear(d1, d2);
        if (comp == 0) {
            comp = compareDatesByField(d1, d2, Calendar.DAY_OF_MONTH);
        }
        return comp;
    }

    /**
     * Itera sobre os dias entre as datas indicadas (inclusive). É o equivalente a chamar
     * {@link #daysBetween(Date, Date, TimeZone, IterationStep)} com o {@link TimeZone} padrão - {@link TimeZone#getDefault()}.
     * 
     * @param start {@link Date}
     * @param end {@link Date}
     * @param step {@link IterationStep}
     */
    public static void daysBetween(final Date start, final Date end, final IterationStep<Date> step) {
        daysBetween(start, end, TimeZone.getDefault(), step);
    }

    /**
     * Itera sobre os dias entre as datas indicadas (inclusive)
     * 
     * @param start data inicial
     * @param end data final
     * @param step {@link IterationStep}
     * @param timezone o {@link TimeZone} a ser utilizado
     */
    public static void daysBetween(final Date start, final Date end, final TimeZone timezone, final IterationStep<Date> step) {
        iterateWithCalendar(start, end, timezone, new CalendarToDateIterationStep(step));
    }

    /**
     * Itera sobre os dias usando um Calendar os dias entre as datas datas (inclusive).
     * 
     * @param start data inicial
     * @param end data final
     * @param zone {@link TimeZone}
     * @param step {@link IterationStep} de {@link Calendar}
     */
    public static void iterateWithCalendar(final Date start, final Date end, final TimeZone zone, final IterationStep<Calendar> step) {
        Calendar calendar = Calendar.getInstance(zone);
        calendar.setTime(start);
        clearTimeFields(calendar);
        final long limit = end.getTime();
        while (calendar.getTimeInMillis() <= limit && step.nextStep(calendar)) {
            calendar.add(Calendar.DAY_OF_YEAR, 1);
        }
    }

    /**
     * Itera sobre os dias úteis entre as datas indicadas (inclusive), sobre o {@link TimeZone} dado e feriados. A lista de feriados deve
     * estar ordenada em ordem natural da data, caso contrário o resultado desta chamado não é determinada.
     * 
     * @param start {@link Date} data inicial
     * @param end {@link Date} data final
     * @param zone {@link TimeZone} a ser usado
     * @param holidays {@link List} de {@link Date} feriados
     * @param step {@link IterationStep}
     */
    @SuppressWarnings("unchecked")
    public static void workDaysBetween(final Date start, final Date end, final TimeZone zone, final List<Date> holidays,
            final IterationStep<Date> step) {
        final List<Date> actualList = holidays != null ? holidays : Collections.EMPTY_LIST;
        iterateWithCalendar(start, end, zone, new IterationStep<Calendar>() {
            public boolean nextStep(final Calendar it) {
                int dow = it.get(Calendar.DAY_OF_WEEK);
                if (dow != Calendar.SUNDAY && dow != Calendar.SATURDAY) {
                    Date current = it.getTime();
                    if (Collections.binarySearch(actualList, current) < 0) {
                        return step.nextStep(current);
                    }
                }
                return true;
            }
        });
    }

    /**
     * Itera sobre os meses entre as datas dadas, inclusive. O {@link Date} passado ao iteration step tem como dia o dia dado por
     * dayOfMonth.
     * 
     * @param start {@link Date} data inicial
     * @param end {@link Date} data final
     * @param dayOfMonth int dia do mes
     * @param step {@link IterationStep}
     */
    public static void monthsBetween(final Date start, final Date end, final int dayOfMonth, final IterationStep<Date> step) {
        if (start == null || end == null || step == null) {
            return;
        }
        Calendar startCal = Calendar.getInstance();
        startCal.setTime(start);
        clearTimeFields(startCal);
        Calendar endCal = Calendar.getInstance();
        endCal.setTime(end);
        final int stopYear = endCal.get(Calendar.YEAR);
        final int stopMonth = endCal.get(Calendar.MONTH);
        while ((startCal.get(Calendar.YEAR) == stopYear && startCal.get(Calendar.MONTH) <= stopMonth)
                || startCal.get(Calendar.YEAR) < stopYear) {
            startCal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
            if (!step.nextStep(startCal.getTime())) {
                break;
            }
            startCal.add(Calendar.MONTH, 1);
        }
    }

    /**
     * @param start {@link Date}
     * @param end {@link Date}
     * @param step {@link IterationStep}
     */
    public static void monthsBetweenWithPartialDate(final Date start, final Date end, final IterationStep<PartialDate> step) {
        if (start == null || end == null || step == null) {
            return;
        }
        Calendar startCal = Calendar.getInstance();
        startCal.setTime(start);
        clearTimeFields(startCal);
        Calendar endCal = Calendar.getInstance();
        endCal.setTime(end);
        clearTimeFields(endCal);
        final int stopYear = endCal.get(Calendar.YEAR);
        final int stopMonth = endCal.get(Calendar.MONTH);
        while ((startCal.get(Calendar.YEAR) == stopYear && startCal.get(Calendar.MONTH) <= stopMonth)
                || startCal.get(Calendar.YEAR) < stopYear) {
            PartialDate date = new PartialDate(startCal.get(Calendar.YEAR), startCal.get(Calendar.MONTH));
            if (!step.nextStep(date)) {
                break;
            }
            startCal.add(Calendar.MONTH, 1);
        }
    }

    /**
     * Apaga os campos referentes à hora - millisegundos, segundos, minutos e horas - do calendário dado, modificando o calendario dado.
     * 
     * @param calendar {@link Calendar}
     */
    public static void clearTimeFields(final Calendar calendar) {
        calendar.clear(Calendar.MILLISECOND);
        calendar.clear(Calendar.SECOND);
        calendar.clear(Calendar.MINUTE);
        calendar.clear(Calendar.HOUR_OF_DAY);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
    }

    /**
     * Compara duas datas considerando apenas o ano.
     * 
     * @param d1 a primeira data a ser comparada
     * @param d2 a segunda data a ser comparada
     * @param field o field de Calendar para fazer a comparação
     * @return -1 se d1 menor que d2, 0 se as datas forem iguais e 1 se d1 maior que d2.
     */
    public static int compareDatesByField(final Date d1, final Date d2, final int field) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(d1);
        int y1 = calendar.get(field);

        calendar.setTime(d2);
        int y2 = calendar.get(field);

        if (y1 < y2) {
            return -1;
        } else if (y1 > y2) {
            return 1;
        } else {
            return 0;
        }
    }

    /**
     * Limpa um Date para que contenha apenas dia mês e ano
     * 
     * @param date Date
     * @return Date
     */
    public static Date clean(final Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        clearTimeFields(cal);
        return cal.getTime();
    }

    /**
     * @param date a data para recuperar o mês
     * @return a mês da data passada
     */
    public static int getMonth(final Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get(Calendar.MONTH);
    }

    /**
     * Procura o ultimo dia do mes.
     * 
     * @param month int mes, como definido nas constanstes de {@link Calendar}
     * @param year ano
     * @return {@link Date}
     */
    public static Date getLastDayOfMonth(final int month, final int year) {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.DAY_OF_MONTH, 1);
        cal.set(Calendar.YEAR, year);
        cal.set(Calendar.MONTH, month + 1);
        clearTimeFields(cal);
        cal.add(Calendar.DAY_OF_MONTH, -1);
        return cal.getTime();
    }

    /**
     * Método que faz o parse da String passada para data sem lançar ParseException. Caso não seja possível fazer o parse String passada,
     * devolve null.
     * 
     * @param source a String para se fazer o parse
     * @param dateFormat o formatter de data
     * @return a data parseada, ou null
     */
    public static Date safeParse(final String source, final DateFormat dateFormat) {
        try {
            return dateFormat.parse(source);
        } catch (ParseException e) {
            return null;
        }
    }

    /**
     * Método que faz o parse da String passada para data sem lançar ParseException. Caso não seja possível fazer o parse String passada,
     * devolve null.
     * 
     * @param source a String para se fazer o parse
     * @param format a String para o format
     * @return a data parseada, ou null
     */
    public static Date safeParse(final String source, final String format) {
        DateFormat dateFormat = new SimpleDateFormat(format);
        return safeParse(source, dateFormat);
    }

}
